import abc


class Frequency(object):

    """Enum like class for bar frequencies. Valid values are:
    * **Frequency.TRADE**: The bar represents a single trade.
    * **Frequency.SECOND**: The bar summarizes the trading activity during 1 second.
    * **Frequency.MINUTE**: The bar summarizes the trading activity during 1 minute.
    * **Frequency.HOUR**: The bar summarizes the trading activity during 1 hour.
    * **Frequency.DAY**: The bar summarizes the trading activity during 1 day.
    * **Frequency.WEEK**: The bar summarizes the trading activity during 1 week.
    * **Frequency.MONTH**: The bar summarizes the trading activity during 1 month.
    """

    # It is important for frequency values to get bigger for bigger windows.
    TRADE = -1
    SECOND = 1
    MINUTE = 60
    HOUR = 60*60
    DAY = 24*60*60
    WEEK = 24*60*60*7
    MONTH = 24*60*60*31


class Bar(object):

    """A Bar is a summary of the trading activity for a security in a given period.
    .. note::
        This is a base class and should not be used directly.
    """

    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def set_use_adjusted_value(self, use_adjusted):
        raise NotImplementedError()

    @abc.abstractmethod
    def get_use_adj_value(self):
        raise NotImplementedError()

    @abc.abstractmethod
    def get_date_time(self):
        """Returns the :class:`datetime.datetime`."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_open(self, adjusted=False):
        """Returns the opening price."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_high(self, adjusted=False):
        """Returns the highest price."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_low(self, adjusted=False):
        """Returns the lowest price."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_close(self, adjusted=False):
        """Returns the closing price."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_volume(self):
        """Returns the volume."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_adj_close(self):
        """Returns the adjusted closing price."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_frequency(self):
        """The bar's period."""
        raise NotImplementedError()

    def get_typical_price(self):
        """Returns the typical price."""
        return (self.get_high() + self.get_low() + self.get_close()) / 3.0

    @abc.abstractmethod
    def get_price(self):
        """Returns the closing or adjusted closing price."""
        raise NotImplementedError()

    def get_extra_columns(self):
        return {}


class BasicBar(Bar):
    # Optimization to reduce memory footprint.
    __slots__ = (
        '__date_time',
        '__open',
        '__close',
        '__high',
        '__low',
        '__volume',
        '__adj_close',
        '__frequency',
        '__use_adjusted_value',
        '__extra',
    )

    def __init__(self, date_time, open_, high, low, close, volume, adj_close, frequency, extra={}):
        if high < low:
            raise Exception("high < low on %s" % (date_time))
        elif high < open_:
            raise Exception("high < open on %s" % (date_time))
        elif high < close:
            raise Exception("high < close on %s" % (date_time))
        elif low > open_:
            raise Exception("low > open on %s" % (date_time))
        elif low > close:
            raise Exception("low > close on %s" % (date_time))

        self.__date_time = date_time
        self.__open = open_
        self.__close = close
        self.__high = high
        self.__low = low
        self.__volume = volume
        self.__adj_close = adj_close
        self.__frequency = frequency
        self.__use_adjusted_value = False
        self.__extra = extra

    def __setstate__(self, state):
        (self.__date_time,
            self.__open,
            self.__close,
            self.__high,
            self.__low,
            self.__volume,
            self.__adj_close,
            self.__frequency,
            self.__use_udjusted_value,
            self.__extra) = state

    def __getstate__(self):
        return (
            self.__date_time,
            self.__open,
            self.__close,
            self.__high,
            self.__low,
            self.__volume,
            self.__adj_close,
            self.__frequency,
            self.__use_adjusted_value,
            self.__extra
        )

    def set_use_adjusted_value(self, use_adjusted):
        if use_adjusted and self.__adj_close is None:
            raise Exception("Adjusted close is not available")
        self.__use_adjusted_value = use_adjusted

    def get_use_adj_value(self):
        return self.__use_adjusted_value

    def get_date_time(self):
        return self.__date_time

    def get_open(self, adjusted=False):
        if adjusted:
            if self.__adj_close is None:
                raise Exception("Adjusted close is missing")
            return self.__adj_close * self.__open / float(self.__close)
        else:
            return self.__open

    def get_high(self, adjusted=False):
        if adjusted:
            if self.__adj_close is None:
                raise Exception("Adjusted close is missing")
            return self.__adj_close * self.__high / float(self.__close)
        else:
            return self.__high

    def get_low(self, adjusted=False):
        if adjusted:
            if self.__adj_close is None:
                raise Exception("Adjusted close is missing")
            return self.__adj_close * self.__low / float(self.__close)
        else:
            return self.__low

    def get_close(self, adjusted=False):
        if adjusted:
            if self.__adj_close is None:
                raise Exception("Adjusted close is missing")
            return self.__adj_close
        else:
            return self.__close

    def get_volume(self):
        return self.__volume

    def get_adj_close(self):
        return self.__adj_close

    def get_frequency(self):
        return self.__frequency

    def get_price(self):
        if self.__use_adjusted_value:
            return self.__adj_close
        else:
            return self.__close

    def get_extra_columns(self):
        return self.__extra


class Bars(object):

    """A group of :class:`Bar` objects.
    :param barDict: A map of instrument to :class:`Bar` objects.
    :type barDict: map.
    .. note::
        All bars must have the same datetime.
    """

    def __init__(self, bar_dict):
        if len(bar_dict) == 0:
            raise Exception("No bars supplied")

        # Check that bar datetimes are in sync
        first_date_time = None
        first_instrument = None
        for instrument, current_bar in bar_dict.iteritems():
            if first_date_time is None:
                first_date_time = current_bar.get_date_time()
                first_instrument = instrument
            elif current_bar.get_date_time() != first_date_time:
                raise Exception("Bar data times are not in sync. %s %s != %s %s" % (
                    instrument,
                    current_bar.get_date_time(),
                    first_instrument,
                    first_date_time
                ))

        self.__bar_dict = bar_dict
        self.__date_time = first_date_time

    def __getitem__(self, instrument):
        """Returns the :class:`bar.Bar` for the given instrument.
        If the instrument is not found an exception is raised."""
        return self.__bar_dict[instrument]

    def __contains__(self, instrument):
        """Returns True if a :class:`bar.Bar` for the given instrument is available."""
        return instrument in self.__bar_dict

    def items(self):
        return self.__bar_dict.items()

    def keys(self):
        return self.__bar_dict.keys()

    def get_instruments(self):
        """Returns the instrument symbols."""
        return self.__bar_dict.keys()

    def get_date_time(self):
        """Returns the :class:`datetime.datetime` for this set of bars."""
        return self.__date_time

    def get_bar(self, instrument):
        """Returns the :class:`bar.Bar` for the given instrument or None if the instrument is not found."""
        return self.__bar_dict.get(instrument, None)